import { getElement } from '@vue-cesium/utils/private/dom'
import TimelineHighlightRange from './TimelineHighlightRange'
import TimelineTrack from './TimelineTrack'

let timelineWheelDelta = 1e12

const timelineMouseMode = {
  none: 0,
  scrub: 1,
  slide: 2,
  zoom: 3,
  touchOnly: 4
}
const timelineTouchMode = {
  none: 0,
  scrub: 1,
  slideZoom: 2,
  singleTap: 3,
  ignore: 4
}

const timelineTicScales = [
  0.001,
  0.002,
  0.005,
  0.01,
  0.02,
  0.05,
  0.1,
  0.25,
  0.5,
  1.0,
  2.0,
  5.0,
  10.0,
  15.0,
  30.0,
  60.0, // 1min
  120.0, // 2min
  300.0, // 5min
  600.0, // 10min
  900.0, // 15min
  1800.0, // 30min
  3600.0, // 1hr
  7200.0, // 2hr
  14400.0, // 4hr
  21600.0, // 6hr
  43200.0, // 12hr
  86400.0, // 24hr
  172800.0, // 2days
  345600.0, // 4days
  604800.0, // 7days
  1296000.0, // 15days
  2592000.0, // 30days
  5184000.0, // 60days
  7776000.0, // 90days
  15552000.0, // 180days
  31536000.0, // 365days
  63072000.0, // 2years
  126144000.0, // 4years
  157680000.0, // 5years
  315360000.0, // 10years
  630720000.0, // 20years
  1261440000.0, // 40years
  1576800000.0, // 50years
  3153600000.0, // 100years
  6307200000.0, // 200years
  12614400000.0, // 400years
  15768000000.0, // 500years
  31536000000.0 // 1000years
]

const timelineMonthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
// const timelineMonthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']

export default class VcTimeline {
  container: Element
  _topDiv: HTMLDivElement
  _endJulian: any
  _epochJulian: any
  _lastXPos: any
  _scrubElement: any
  _startJulian: any
  _timeBarSecondsSpan: any
  _clock: Cesium.Clock
  _scrubJulian: Cesium.JulianDate
  _mainTicSpan: number
  _mouseMode: number
  _touchMode: number
  _touchState: { centerX: number, spanX: number }
  _mouseX: number
  _timelineDrag: number
  _timelineDragLocation: any
  _lastHeight: any
  _lastWidth: any
  _timeBarEle: any
  _trackContainer: any
  _trackListEle: any
  _needleEle: any
  _rulerEle: any
  _context: any
  _trackList: any[]
  _highlightRanges: any[]
  _onMouseDown: (e: any) => void
  _onMouseUp: (e: any) => void
  _onMouseMove: (e: any) => void
  _onMouseWheel: (e: any) => void
  _onTouchStart: (e: any) => void
  _onTouchMove: (e: any) => void
  _onTouchEnd: (e: any) => void
  constructor(container: Element, clock: Cesium.Clock) {
    const { defined, DeveloperError } = Cesium
    // >>includeStart('debug', pragmas.debug);
    if (!defined(container)) {
      throw new DeveloperError('container is required.')
    }
    if (!defined(clock)) {
      throw new DeveloperError('clock is required.')
    }
    // >>includeEnd('debug');

    container = getElement(container)

    const ownerDocument = container.ownerDocument

    /**
     * Gets the parent container.
     * @type {Element}
     */
    this.container = container

    const topDiv = ownerDocument.createElement('div')
    topDiv.className = 'cesium-timeline-main'
    container.appendChild(topDiv)
    this._topDiv = topDiv

    this._endJulian = undefined
    this._epochJulian = undefined
    this._lastXPos = undefined
    this._scrubElement = undefined
    this._startJulian = undefined
    this._timeBarSecondsSpan = undefined
    this._clock = clock
    this._scrubJulian = clock.currentTime
    this._mainTicSpan = -1
    this._mouseMode = timelineMouseMode.none
    this._touchMode = timelineTouchMode.none
    this._touchState = {
      centerX: 0,
      spanX: 0
    }
    this._mouseX = 0
    this._timelineDrag = 0
    this._timelineDragLocation = undefined
    this._lastHeight = undefined
    this._lastWidth = undefined

    this._topDiv.innerHTML
      = '<div class="cesium-timeline-bar"></div><div class="cesium-timeline-trackContainer">'
        + '<canvas class="cesium-timeline-tracks" width="10" height="1">'
        + '</canvas></div><div class="cesium-timeline-needle"></div><span class="cesium-timeline-ruler"></span>'
    this._timeBarEle = this._topDiv.childNodes[0]
    this._trackContainer = this._topDiv.childNodes[1]
    this._trackListEle = this._topDiv.childNodes[1].childNodes[0]
    this._needleEle = this._topDiv.childNodes[2]
    this._rulerEle = this._topDiv.childNodes[3]
    this._context = this._trackListEle.getContext('2d')

    this._trackList = []
    this._highlightRanges = []

    this.zoomTo(clock.startTime, clock.stopTime)

    this._onMouseDown = createMouseDownCallback(this)
    this._onMouseUp = createMouseUpCallback(this)
    this._onMouseMove = createMouseMoveCallback(this)
    this._onMouseWheel = createMouseWheelCallback(this)
    this._onTouchStart = createTouchStartCallback(this)
    this._onTouchMove = createTouchMoveCallback(this)
    this._onTouchEnd = createTouchEndCallback(this)

    const timeBarEle = this._timeBarEle
    ownerDocument.addEventListener('mouseup', this._onMouseUp, false)
    ownerDocument.addEventListener('mousemove', this._onMouseMove, false)
    timeBarEle.addEventListener('mousedown', this._onMouseDown, false)
    timeBarEle.addEventListener('DOMMouseScroll', this._onMouseWheel, false) // Mozilla mouse wheel
    timeBarEle.addEventListener('mousewheel', this._onMouseWheel, false)
    timeBarEle.addEventListener('touchstart', this._onTouchStart, false)
    timeBarEle.addEventListener('touchmove', this._onTouchMove, false)
    timeBarEle.addEventListener('touchend', this._onTouchEnd, false)
    timeBarEle.addEventListener('touchcancel', this._onTouchEnd, false)

    this._topDiv.oncontextmenu = function () {
      return false
    }

    clock.onTick.addEventListener(this.updateFromClock, this)
    this.updateFromClock()
  }

  smallestTicInPixels = 7.0

  addEventListener(type, listener, useCapture) {
    this._topDiv.addEventListener(type, listener, useCapture)
  }

  removeEventListener(type, listener, useCapture) {
    this._topDiv.removeEventListener(type, listener, useCapture)
  }

  isDestroyed() {
    return false
  }

  destroy() {
    this._clock.onTick.removeEventListener(this.updateFromClock, this)

    const doc = this.container.ownerDocument
    doc.removeEventListener('mouseup', this._onMouseUp, false)
    doc.removeEventListener('mousemove', this._onMouseMove, false)

    const timeBarEle = this._timeBarEle
    timeBarEle.removeEventListener('mousedown', this._onMouseDown, false)
    timeBarEle.removeEventListener('DOMMouseScroll', this._onMouseWheel, false) // Mozilla mouse wheel
    timeBarEle.removeEventListener('mousewheel', this._onMouseWheel, false)
    timeBarEle.removeEventListener('touchstart', this._onTouchStart, false)
    timeBarEle.removeEventListener('touchmove', this._onTouchMove, false)
    timeBarEle.removeEventListener('touchend', this._onTouchEnd, false)
    timeBarEle.removeEventListener('touchcancel', this._onTouchEnd, false)
    this.container.removeChild(this._topDiv)
    Cesium.destroyObject(this)
  }

  addHighlightRange(color, heightInPx, base) {
    const newHighlightRange = new TimelineHighlightRange(color, heightInPx, base)
    this._highlightRanges.push(newHighlightRange)
    this.resize()
    return newHighlightRange
  }

  addTrack(interval, heightInPx, color, backgroundColor) {
    // console.log('addTrack', interval, heightInPx, color, backgroundColor)
    const newTrack = new TimelineTrack(interval, heightInPx, color, backgroundColor)
    this._trackList.push(newTrack)
    this._lastHeight = undefined
    this.resize()
    return newTrack
  }

  resize() {
    const width = this.container.clientWidth
    const height = this.container.clientHeight

    if (width === this._lastWidth && height === this._lastHeight) {
      return
    }

    this._trackContainer.style.height = `${height}px`

    let trackListHeight = 1
    this._trackList.forEach((track) => {
      trackListHeight += track.height
    })
    this._trackListEle.style.height = `${trackListHeight.toString()}px`
    this._trackListEle.width = this._trackListEle.clientWidth
    this._trackListEle.height = trackListHeight
    this._makeTics()

    this._lastXPos = undefined
    this._lastWidth = width
    this._lastHeight = height
  }

  zoomTo(startTime, stopTime) {
    const { defined, JulianDate, DeveloperError, ClockRange } = Cesium
    // >>includeStart('debug', pragmas.debug);
    if (!defined(startTime)) {
      throw new DeveloperError('startTime is required.')
    }
    if (!defined(stopTime)) {
      throw new DeveloperError('stopTime is required')
    }
    if (JulianDate.lessThanOrEquals(stopTime, startTime)) {
      throw new DeveloperError('Start time must come before end time.')
    }
    // >>includeEnd('debug');

    this._startJulian = startTime
    this._endJulian = stopTime
    this._timeBarSecondsSpan = JulianDate.secondsDifference(stopTime, startTime)

    // If clock is not unbounded, clamp timeline range to clock.
    if (this._clock && this._clock.clockRange !== ClockRange.UNBOUNDED) {
      const clockStart = this._clock.startTime
      const clockEnd = this._clock.stopTime
      const clockSpan = JulianDate.secondsDifference(clockEnd, clockStart)
      const startOffset = JulianDate.secondsDifference(clockStart, this._startJulian)
      const endOffset = JulianDate.secondsDifference(clockEnd, this._endJulian)

      if (this._timeBarSecondsSpan >= clockSpan) {
        // if new duration longer than clock range duration, clamp to full range.
        this._timeBarSecondsSpan = clockSpan
        this._startJulian = this._clock.startTime
        this._endJulian = this._clock.stopTime
      }
      else if (startOffset > 0) {
        // if timeline start is before clock start, shift right
        this._endJulian = JulianDate.addSeconds(this._endJulian, startOffset, new JulianDate())
        this._startJulian = clockStart
        this._timeBarSecondsSpan = JulianDate.secondsDifference(this._endJulian, this._startJulian)
      }
      else if (endOffset < 0) {
        // if timeline end is after clock end, shift left
        this._startJulian = JulianDate.addSeconds(this._startJulian, endOffset, new JulianDate())
        this._endJulian = clockEnd
        this._timeBarSecondsSpan = JulianDate.secondsDifference(this._endJulian, this._startJulian)
      }
    }

    this._makeTics()

    // const evt = document.createEvent('Event')
    const evt: any = new Event('setzoom', { bubbles: true, cancelable: true, composed: true })
    // evt.initEvent('setzoom', true, true)
    evt.startJulian = this._startJulian
    evt.endJulian = this._endJulian
    evt.epochJulian = this._epochJulian
    evt.totalSpan = this._timeBarSecondsSpan
    evt.mainTicSpan = this._mainTicSpan
    this._topDiv.dispatchEvent(evt)
  }

  updateFromClock() {
    const { defined, JulianDate } = Cesium
    this._scrubJulian = this._clock.currentTime
    const scrubElement = this._scrubElement
    if (defined(this._scrubElement)) {
      const seconds = JulianDate.secondsDifference(this._scrubJulian, this._startJulian)
      const xPos = Math.round((seconds * this._topDiv.clientWidth) / this._timeBarSecondsSpan)

      if (this._lastXPos !== xPos) {
        this._lastXPos = xPos

        scrubElement.style.left = `${xPos - 8}px`
        this._needleEle.style.left = `${xPos}px`
      }
    }
    if (defined(this._timelineDragLocation)) {
      this._setTimeBarTime(this._timelineDragLocation, (this._timelineDragLocation * this._timeBarSecondsSpan) / this._topDiv.clientWidth)
      this.zoomTo(
        JulianDate.addSeconds(this._startJulian, this._timelineDrag, new JulianDate()),
        JulianDate.addSeconds(this._endJulian, this._timelineDrag, new JulianDate())
      )
    }
  }

  _setTimeBarTime(xPos, seconds) {
    const { JulianDate } = Cesium
    xPos = Math.round(xPos)
    this._scrubJulian = JulianDate.addSeconds(this._startJulian, seconds, new JulianDate())
    if (this._scrubElement) {
      const scrubX = xPos - 8
      this._scrubElement.style.left = `${scrubX.toString()}px`
      this._needleEle.style.left = `${xPos.toString()}px`
    }

    // const evt = document.createEvent('Event')
    // evt.initEvent('settime', true, true)
    const evt: any = new Event('settime', { bubbles: true, cancelable: true, composed: true })
    evt.clientX = xPos
    evt.timeSeconds = seconds
    evt.timeJulian = this._scrubJulian
    evt.clock = this._clock
    this._topDiv.dispatchEvent(evt)
  }

  zoomFrom(amount) {
    const { JulianDate } = Cesium
    let centerSec = JulianDate.secondsDifference(this._scrubJulian, this._startJulian)
    if (amount > 1 || centerSec < 0 || centerSec > this._timeBarSecondsSpan) {
      centerSec = this._timeBarSecondsSpan * 0.5
    }
    else {
      centerSec += centerSec - this._timeBarSecondsSpan * 0.5
    }
    const centerSecFlip = this._timeBarSecondsSpan - centerSec
    this.zoomTo(
      JulianDate.addSeconds(this._startJulian, centerSec - centerSec * amount, new JulianDate()),
      JulianDate.addSeconds(this._endJulian, centerSecFlip * amount - centerSecFlip, new JulianDate())
    )
  }

  makeLabel(time) {
    const { JulianDate } = Cesium
    const gregorian = JulianDate.toGregorianDate(time)
    const millisecond = gregorian.millisecond
    let millisecondString = ' UTC'
    if (millisecond > 0 && this._timeBarSecondsSpan < 3600) {
      millisecondString = Math.floor(millisecond).toString()
      while (millisecondString.length < 3) {
        millisecondString = `0${millisecondString}`
      }
      millisecondString = `.${millisecondString}`
    }

    return `${timelineMonthNames[gregorian.month - 1]} ${gregorian.day} ${gregorian.year} ${twoDigits(gregorian.hour)}:${twoDigits(
      gregorian.minute
    )}:${twoDigits(gregorian.second)}${millisecondString}`
  }

  _makeTics() {
    const { JulianDate } = Cesium
    const timeBar = this._timeBarEle

    const seconds = JulianDate.secondsDifference(this._scrubJulian, this._startJulian)
    const xPos = Math.round((seconds * this._topDiv.clientWidth) / this._timeBarSecondsSpan)
    const scrubX = xPos - 8
    let tic
    const widget = this

    this._needleEle.style.left = `${xPos.toString()}px`

    let tics = ''

    const minimumDuration = 0.01
    const maximumDuration = 31536000000.0 // ~1000 years
    const epsilon = 1e-10

    // If time step size is known, enter it here...
    let minSize = 0

    let duration = this._timeBarSecondsSpan
    if (duration < minimumDuration) {
      duration = minimumDuration
      this._timeBarSecondsSpan = minimumDuration
      this._endJulian = JulianDate.addSeconds(this._startJulian, minimumDuration, new JulianDate())
    }
    else if (duration > maximumDuration) {
      duration = maximumDuration
      this._timeBarSecondsSpan = maximumDuration
      this._endJulian = JulianDate.addSeconds(this._startJulian, maximumDuration, new JulianDate())
    }

    let timeBarWidth = this._timeBarEle.clientWidth
    if (timeBarWidth < 10) {
      timeBarWidth = 10
    }
    const startJulian = this._startJulian

    // epsilonTime: a small fraction of one pixel width of the timeline, measured in seconds.
    const epsilonTime = Math.min((duration / timeBarWidth) * 1e-5, 0.4)

    // epochJulian: a nearby time to be considered "zero seconds", should be a round-ish number by human standards.
    let epochJulian
    const gregorianDate = JulianDate.toGregorianDate(startJulian)
    if (duration > 315360000) {
      // 3650+ days visible, epoch is start of the first visible century.
      epochJulian = JulianDate.fromDate(new Date(Date.UTC(Math.floor(gregorianDate.year / 100) * 100, 0)))
    }
    else if (duration > 31536000) {
      // 365+ days visible, epoch is start of the first visible decade.
      epochJulian = JulianDate.fromDate(new Date(Date.UTC(Math.floor(gregorianDate.year / 10) * 10, 0)))
    }
    else if (duration > 86400) {
      // 1+ day(s) visible, epoch is start of the year.
      epochJulian = JulianDate.fromDate(new Date(Date.UTC(gregorianDate.year, 0)))
    }
    else {
      // Less than a day on timeline, epoch is midnight of the visible day.
      epochJulian = JulianDate.fromDate(new Date(Date.UTC(gregorianDate.year, gregorianDate.month, gregorianDate.day)))
    }

    // startTime: Seconds offset of the left side of the timeline from epochJulian.
    const startTime = JulianDate.secondsDifference(this._startJulian, JulianDate.addSeconds(epochJulian, epsilonTime, new JulianDate()))
    // endTime: Seconds offset of the right side of the timeline from epochJulian.
    let endTime = startTime + duration
    this._epochJulian = epochJulian

    function getStartTic(ticScale) {
      return Math.floor(startTime / ticScale) * ticScale
    }

    function getNextTic(tic, ticScale) {
      return Math.ceil(tic / ticScale + 0.5) * ticScale
    }

    function getAlpha(time) {
      return (time - startTime) / duration
    }

    function remainder(x, y) {
      // return x % y;
      return x - y * Math.round(x / y)
    }

    // Width in pixels of a typical label, plus padding
    this._rulerEle.innerHTML = this.makeLabel(JulianDate.addSeconds(this._endJulian, -minimumDuration, new JulianDate()))
    let sampleWidth = this._rulerEle.offsetWidth + 20
    if (sampleWidth < 30) {
      // Workaround an apparent IE bug with measuring the width after going full-screen from inside an iframe.
      sampleWidth = 180
    }

    const origMinSize = minSize
    minSize -= epsilon

    const renderState: any = {
      startTime,
      startJulian,
      epochJulian,
      duration,
      timeBarWidth,
      getAlpha
    }
    this._highlightRanges.forEach((highlightRange) => {
      tics += highlightRange.render(renderState)
    })

    // Calculate tic mark label spacing in the TimeBar.
    let mainTic = 0.0
    let subTic = 0.0
    let tinyTic = 0.0
    // Ideal labeled tic as percentage of zoom interval
    let idealTic = sampleWidth / timeBarWidth
    if (idealTic > 1.0) {
      // Clamp to width of window, for thin windows.
      idealTic = 1.0
    }
    // Ideal labeled tic size in seconds
    idealTic *= this._timeBarSecondsSpan
    let ticIndex = -1
    let smallestIndex = -1

    const ticScaleLen = timelineTicScales.length
    let i
    for (i = 0; i < ticScaleLen; ++i) {
      const sc = timelineTicScales[i]
      ++ticIndex
      mainTic = sc
      // Find acceptable main tic size not smaller than ideal size.
      if (sc > idealTic && sc > minSize) {
        break
      }
      if (smallestIndex < 0 && timeBarWidth * (sc / this._timeBarSecondsSpan) >= this.smallestTicInPixels) {
        smallestIndex = ticIndex
      }
    }
    if (ticIndex > 0) {
      while (ticIndex > 0) {
        // Compute sub-tic size that evenly divides main tic.
        --ticIndex
        if (Math.abs(remainder(mainTic, timelineTicScales[ticIndex])) < 0.00001) {
          if (timelineTicScales[ticIndex] >= minSize) {
            subTic = timelineTicScales[ticIndex]
          }
          break
        }
      }

      if (smallestIndex >= 0) {
        while (smallestIndex < ticIndex) {
          // Compute tiny tic size that evenly divides sub-tic.
          if (Math.abs(remainder(subTic, timelineTicScales[smallestIndex])) < 0.00001 && timelineTicScales[smallestIndex] >= minSize) {
            tinyTic = timelineTicScales[smallestIndex]
            break
          }
          ++smallestIndex
        }
      }
    }

    minSize = origMinSize
    if (minSize > epsilon && tinyTic < 0.00001 && Math.abs(minSize - mainTic) > epsilon) {
      tinyTic = minSize
      if (minSize <= mainTic + epsilon) {
        subTic = 0.0
      }
    }

    let lastTextLeft = -999999
    let textWidth
    if (timeBarWidth * (tinyTic / this._timeBarSecondsSpan) >= 3.0) {
      for (tic = getStartTic(tinyTic); tic <= endTime; tic = getNextTic(tic, tinyTic)) {
        tics += `<span class="cesium-timeline-ticTiny" style="left: ${Math.round(timeBarWidth * getAlpha(tic)).toString()}px;"></span>`
      }
    }
    if (timeBarWidth * (subTic / this._timeBarSecondsSpan) >= 3.0) {
      for (tic = getStartTic(subTic); tic <= endTime; tic = getNextTic(tic, subTic)) {
        tics += `<span class="cesium-timeline-ticSub" style="left: ${Math.round(timeBarWidth * getAlpha(tic)).toString()}px;"></span>`
      }
    }
    if (timeBarWidth * (mainTic / this._timeBarSecondsSpan) >= 2.0) {
      this._mainTicSpan = mainTic
      endTime += mainTic
      tic = getStartTic(mainTic)
      const leapSecond = JulianDate.computeTaiMinusUtc(epochJulian)
      while (tic <= endTime) {
        let ticTime = JulianDate.addSeconds(startJulian, tic - startTime, new JulianDate())
        if (mainTic > 2.1) {
          const ticLeap = JulianDate.computeTaiMinusUtc(ticTime)
          if (Math.abs(ticLeap - leapSecond) > 0.1) {
            tic += ticLeap - leapSecond
            ticTime = JulianDate.addSeconds(startJulian, tic - startTime, new JulianDate())
          }
        }
        const ticLeft = Math.round(timeBarWidth * getAlpha(tic))
        const ticLabel = this.makeLabel(ticTime)
        this._rulerEle.innerHTML = ticLabel
        textWidth = this._rulerEle.offsetWidth
        if (textWidth < 10) {
          // IE iframe fullscreen sampleWidth workaround, continued.
          textWidth = sampleWidth
        }
        const labelLeft = ticLeft - (textWidth / 2 - 1)
        if (labelLeft > lastTextLeft) {
          lastTextLeft = labelLeft + textWidth + 5
          tics
            += `<span class="cesium-timeline-ticMain" style="left: ${ticLeft.toString()}px;"></span>`
              + `<span class="cesium-timeline-ticLabel" style="left: ${labelLeft.toString()}px;">${ticLabel}</span>`
        }
        else {
          tics += `<span class="cesium-timeline-ticSub" style="left: ${ticLeft.toString()}px;"></span>`
        }
        tic = getNextTic(tic, mainTic)
      }
    }
    else {
      this._mainTicSpan = -1
    }

    tics += `<span class="cesium-timeline-icon16" style="left:${scrubX}px;bottom:0;background-position: 0 0;"></span>`
    timeBar.innerHTML = tics
    this._scrubElement = timeBar.lastChild

    // Clear track canvas.
    this._context.clearRect(0, 0, this._trackListEle.width, this._trackListEle.height)

    renderState.y = 0
    this._trackList.forEach((track) => {
      track.render(widget._context, renderState)
      renderState.y += track.height
    })
  }
}

function createMouseDownCallback(timeline) {
  return function (e) {
    if (timeline._mouseMode !== timelineMouseMode.touchOnly) {
      if (e.button === 0) {
        timeline._mouseMode = timelineMouseMode.scrub
        if (timeline._scrubElement) {
          timeline._scrubElement.style.backgroundPosition = '-16px 0'
        }
        timeline._onMouseMove(e)
      }
      else {
        timeline._mouseX = e.clientX
        if (e.button === 2) {
          timeline._mouseMode = timelineMouseMode.zoom
        }
        else {
          timeline._mouseMode = timelineMouseMode.slide
        }
      }
    }
    e.preventDefault()
  }
}

function createMouseUpCallback(timeline) {
  return function (e) {
    timeline._mouseMode = timelineMouseMode.none
    if (timeline._scrubElement) {
      timeline._scrubElement.style.backgroundPosition = '0 0'
    }
    timeline._timelineDrag = 0
    timeline._timelineDragLocation = undefined
  }
}

function createMouseMoveCallback(timeline) {
  return function (e) {
    let dx
    if (timeline._mouseMode === timelineMouseMode.scrub) {
      e.preventDefault()
      const x = e.clientX - timeline._topDiv.getBoundingClientRect().left
      // console.log(`createMouseMoveCallback scrub; x: ${x}; clientWidth: ${timeline._topDiv.clientWidth};`)
      if (x < 0) {
        timeline._timelineDragLocation = 0
        timeline._timelineDrag = -0.01 * timeline._timeBarSecondsSpan
      }
      else if (x > timeline._topDiv.clientWidth) {
        timeline._timelineDragLocation = timeline._topDiv.clientWidth
        timeline._timelineDrag = 0.01 * timeline._timeBarSecondsSpan
      }
      else {
        // console.log('_setTimeBarTime')
        timeline._timelineDragLocation = undefined
        timeline._setTimeBarTime(x, (x * timeline._timeBarSecondsSpan) / timeline._topDiv.clientWidth)
      }
    }
    else if (timeline._mouseMode === timelineMouseMode.slide) {
      // console.log('createMouseMoveCallback slide')
      dx = timeline._mouseX - e.clientX
      timeline._mouseX = e.clientX
      if (dx !== 0) {
        const { JulianDate } = Cesium
        const dsec = (dx * timeline._timeBarSecondsSpan) / timeline._topDiv.clientWidth
        timeline.zoomTo(
          JulianDate.addSeconds(timeline._startJulian, dsec, new JulianDate()),
          JulianDate.addSeconds(timeline._endJulian, dsec, new JulianDate())
        )
      }
    }
    else if (timeline._mouseMode === timelineMouseMode.zoom) {
      // console.log('createMouseMoveCallback zoom')
      dx = timeline._mouseX - e.clientX
      timeline._mouseX = e.clientX
      if (dx !== 0) {
        timeline.zoomFrom(1.01 ** dx)
      }
    }
  }
}

function createMouseWheelCallback(timeline) {
  return function (e) {
    let dy = e.wheelDeltaY || e.wheelDelta || -e.detail
    timelineWheelDelta = Math.max(Math.min(Math.abs(dy), timelineWheelDelta), 1)
    dy /= timelineWheelDelta
    timeline.zoomFrom(1.05 ** -dy)
  }
}

function createTouchStartCallback(timeline) {
  return function (e) {
    const len = e.touches.length
    let seconds, xPos
    const leftX = timeline._topDiv.getBoundingClientRect().left
    e.preventDefault()
    timeline._mouseMode = timelineMouseMode.touchOnly
    if (len === 1) {
      seconds = Cesium.JulianDate.secondsDifference(timeline._scrubJulian, timeline._startJulian)
      xPos = Math.round((seconds * timeline._topDiv.clientWidth) / timeline._timeBarSecondsSpan + leftX)
      if (Math.abs(e.touches[0].clientX - xPos) < 50) {
        timeline._touchMode = timelineTouchMode.scrub
        if (timeline._scrubElement) {
          timeline._scrubElement.style.backgroundPosition = len === 1 ? '-16px 0' : '0 0'
        }
      }
      else {
        timeline._touchMode = timelineTouchMode.singleTap
        timeline._touchState.centerX = e.touches[0].clientX - leftX
      }
    }
    else if (len === 2) {
      timeline._touchMode = timelineTouchMode.slideZoom
      timeline._touchState.centerX = (e.touches[0].clientX + e.touches[1].clientX) * 0.5 - leftX
      timeline._touchState.spanX = Math.abs(e.touches[0].clientX - e.touches[1].clientX)
    }
    else {
      timeline._touchMode = timelineTouchMode.ignore
    }
  }
}

function createTouchEndCallback(timeline) {
  return function (e) {
    const len = e.touches.length
    const leftX = timeline._topDiv.getBoundingClientRect().left
    if (timeline._touchMode === timelineTouchMode.singleTap) {
      timeline._touchMode = timelineTouchMode.scrub
      timeline._onTouchMove(e)
    }
    else if (timeline._touchMode === timelineTouchMode.scrub) {
      timeline._onTouchMove(e)
    }
    timeline._mouseMode = timelineMouseMode.touchOnly
    if (len !== 1) {
      timeline._touchMode = len > 0 ? timelineTouchMode.ignore : timelineTouchMode.none
    }
    else if (timeline._touchMode === timelineTouchMode.slideZoom) {
      timeline._touchState.centerX = e.touches[0].clientX - leftX
    }
    if (timeline._scrubElement) {
      timeline._scrubElement.style.backgroundPosition = '0 0'
    }
  }
}

function createTouchMoveCallback(timeline) {
  return function (e) {
    let dx
    let x
    let len
    let newCenter
    let newSpan
    let newStartTime
    let zoom = 1
    const leftX = timeline._topDiv.getBoundingClientRect().left
    if (timeline._touchMode === timelineTouchMode.singleTap) {
      timeline._touchMode = timelineTouchMode.slideZoom
    }
    timeline._mouseMode = timelineMouseMode.touchOnly
    if (timeline._touchMode === timelineTouchMode.scrub) {
      e.preventDefault()
      if (e.changedTouches.length === 1) {
        x = e.changedTouches[0].clientX - leftX
        if (x >= 0 && x <= timeline._topDiv.clientWidth) {
          timeline._setTimeBarTime(x, (x * timeline._timeBarSecondsSpan) / timeline._topDiv.clientWidth)
        }
      }
    }
    else if (timeline._touchMode === timelineTouchMode.slideZoom) {
      len = e.touches.length
      if (len === 2) {
        newCenter = (e.touches[0].clientX + e.touches[1].clientX) * 0.5 - leftX
        newSpan = Math.abs(e.touches[0].clientX - e.touches[1].clientX)
      }
      else if (len === 1) {
        newCenter = e.touches[0].clientX - leftX
        newSpan = 0
      }

      const { defined, JulianDate } = Cesium

      if (defined(newCenter)) {
        if (newSpan > 0 && timeline._touchState.spanX > 0) {
          // Zoom and slide
          zoom = timeline._touchState.spanX / newSpan
          newStartTime = JulianDate.addSeconds(
            timeline._startJulian,
            (timeline._touchState.centerX * timeline._timeBarSecondsSpan - newCenter * timeline._timeBarSecondsSpan * zoom)
            / timeline._topDiv.clientWidth,
            new JulianDate()
          )
        }
        else {
          // Slide to newCenter
          dx = timeline._touchState.centerX - newCenter
          newStartTime = JulianDate.addSeconds(
            timeline._startJulian,
            (dx * timeline._timeBarSecondsSpan) / timeline._topDiv.clientWidth,
            new JulianDate()
          )
        }

        timeline.zoomTo(newStartTime, JulianDate.addSeconds(newStartTime, timeline._timeBarSecondsSpan * zoom, new JulianDate()))
        timeline._touchState.centerX = newCenter
        timeline._touchState.spanX = newSpan
      }
    }
  }
}

function twoDigits(num) {
  return num < 10 ? `0${num.toString()}` : num.toString()
}
